Vue 3.0 源码解读¶
导读¶
发展历程¶
- Vue.js 1.x --(引入虚拟DOM)→ Vue.js 2.x
-
Vue.js 2.x 痛点,Vue 3.0 诞生
- 源码自身的可维护性:Vue 2.x 数据量大后的渲染/更新性能问题
- 兼容性:Vue 2.x 想舍弃但为了兼容仍旧保留的鸡肋API,如 Mixin 等
- 更好的编程体验
- 更好的 TypeScript 支持
- 更好的逻辑复用实践
源码优化¶
- 目的:让源码更易于开发和维护
- 手段:使用 Monorepo 和 TypeScript 管理和开发源码,提升可维护性。
-
优化实践 Monorepo
-
-
compiler:模板编译相关代码
- parser
- directives
- codegen
-
core:平台无关的通用运行时代码
- components 内置组件:如 keep-alive
- global-api:如 extend.js、mixin.js、use.js、assets.js
- instance:如 render-helpers、events.js、init.js、lifecycle.js、proxy.js、render.js、state.js、inject.js
- observer:如 array.js、dep.js、scheduler.js、traverse.js、watcher.js
- utils:如 env.js、error.js、next-tick.js、options.js、props.js
- vdom:如 create-component.js、create-element.js、create-functional-component.js、patch.js、vnode.js
-
platforms:平台专有代码
- web:compiler.js、runtime.js等
- weex
-
server:服务端渲染相关代码
- bundle-renderer
- template-renderer
- optimizing-compiler
- webpack-plugin
- create-basic-renderer.js
- create-renderer.js
- render.js
- render-context.js
- render-stream.js
-
sfc:
.vue
单文件解析相关代码- parser.js
-
Shared:共享工具代码
- constants.js
- utils.js
-
-
- reactivity: 响应式系统
- runtime-core:与平台无关的运行时核心
- runtime-dom:针对浏览器的运行时
- compiler-core:与平台无关的编译核心
- compiler-dom:针对浏览器的编译模块
- compiler-ssr:针对服务端渲染的编译模块
- compiler-sfc:针对
.vue
单文件解析 - shared:多个包之间共享的内容
- server-renderer:用于服务端渲染
- vue:完整版本,包括运行时和编译器
-
好处
- 相比于 Vue 2.x 的代码组织,monorepo 把这些代码模块拆分到不同 packages 中,每个 package 有各自的 API、类型定义和测试。
- 拆分更细化,职责划分更明确,模块之间的依赖关系更清新
- 开发人员更容易阅读、理解和变更所有源码,提升可维护性
- package 可以独立于 Vue.js 使用;按需使用,体积更小。
-
-
优化实践 TypeScript
-
Vue2.x 使用 Flow
- Flow 是 Facebook 出品的 Javascript 静态类型检查工具,能 以很小成本 对已有的 Javascript 代码迁入,非常灵活。
- 但 Flow 对一些 复杂场景的类型检查,支持不够好
-
Vue3.0 使用 TypeScript
- 可以在编码期间完成 类型检查,避免因类型问题导致的错误
- 有利于定义接口类型,利于IDE对变量类型的推导
- TypeScript 生态更完善,并且保持一定频率的更新
-
性能优化¶
-
源码体积优化
- 移除一些冷门的 Feature,比如 filter、inline-template等
-
引入 tree-shaking 的技术
- 依赖ES2015模块语法静态结构(即import/export),通过 编译阶段 的静态分析,找到没有引入的模块并打上标记;压缩阶段,利用uglify-js、terser 等压缩工具删除没用的代码。
- 如果项目中没有引用 Transition、KeepAlive 等组件,那么打包就会移除,从而减少项目引入Vue.js 体积
-
数据劫持优化
- 思路:渲染DOM时,访问了数据,对访问进行劫持;通过劫持数据的访问和更新,来实现DOM更新功能。
- vue 1.x
- vue 2.x & vue 3.0
-
vue 2.x 使用
Object.defineProperty()
缺陷- 必须预先知道要劫持的 key 是什么
- 不能监测到对象的添加和删除,只能通过 \(set、\)delete 实例方法实现
- 对于深层嵌套对象,只能递归遍历对象;存在性能问题
-
vue 3.0 使用
new Proxy()
- 对于增加和删除都能监测
- Proxy API 不能监听深层次对象变化,那么Vue.js 3.0 处理方式是在getter中去递归响应式;即只能在真正访问到的内部对象才会变成响应式(惰性监测)
编译优化¶
-
Vue 2.x
- 全流程图
- template -- compile → render function 阶段可以借助 vue-loader 在 webpack 编译阶段离线完成,不必优化
- 思路:重点优化相对耗时的 patch 阶段;vue 2.x 数据更新触发重新渲染的粒度是组件级的;diff 时,对于静态节点的遍历都是不必要的。
- 渲染管线图
-
vue 3.0
- patch 优化:通过编译阶段对静态模版的分析,编译生成 Block Tree;
- Block Tree:是一个将模板 基于动态节点指令 切割的嵌套 Block,每个 Block 内部节点结构是 固定的,每个 Block 只需维护一个 Array 来追踪包含的动态节点。
- 借助 Block Tree,实现了巨大性能突破,从整体规模相关到只与动态内容规模相关。
- 只把绑定数据的动态节点加入嵌套区块数据,每个区块以数组追踪。
-
此外
- Slot 编译优化
- 事件侦听函数缓存
- 运行时重写 diff 算法
语法优化¶
-
优化逻辑组织
-
Vue 2.x/Options API 思想
- 编写组件本质就是在写一个 「包含了描述组件options 的对象」
-
优点
- 写法符合直觉思维,新手友好。
- Options API 按照 methods、computed、data、props 等不同选项进行分类;
- 当组件小时,这种分类方式一目了然。
-
缺点
- 但在大型组件中,一个组件可能有多个 逻辑关注点;
- 每个「关注点」都有自己的 options,如果需要修改一个逻辑关注点,需要在单文件中不断上下切换与寻找
-
Vue 3.0/Composition API
- 将逻辑关注点相关的代码,放在一个函数中;
- 这样修改一个功能时,不需要在文件中跳来跳去。
-
对比图
-
vue 2.x 用 mixins 去复用逻辑
- 但当大量 mixins 被使用,会造成「命名冲突」和「数据来源不明确」:
- 每个 mixin 都可以定义自己的 props、data、methods,且相互之间无感知,容易定义相同变量,导致「命名冲突」。
- 对组件而言,如果模板中使用不在当前组件中定义的变量,那么就难以知道变量在哪里定义,即「数据来源不明确」。
-
vue 3 的 Composition API 很好解决上述问题。
-
-
更好的类型支持
- Composition API 都是函数,在函数调用时,所有的类型就自然被推导出来;
- 不像 Options API,所有东西都使用 this
-
Tree-shaking 友好
- Vue 3.0/Composition API tree-shaking 友好,让代码更容易压缩
大规模启用 RFC¶
- RFC (Request For Comments),使每个版本改动可控
- 为新功能进入框架提供一个一致且受控的路径
- 了解每一 feature 采纳或废弃的前因后果
Vue.js 核心组件实现¶
组件渲染¶
组件更新¶
Composition API¶
编译过程及优化思想¶
实用特性及原理¶
内置组件及实现原理¶
官方生态的实现原理¶
未整理¶
1、新特性¶
- Performance: 性能提升 1.2 ~ 2 倍
- Tree-shaking 支持: 按需加载,体积更小
- CompositionAPI: 类似 React Hooks
- 更好的 TS 支持
- Custom Render API: 暴露自定义 API
- Fragment, Teleport, Suspense 组件
2、如何做到提速¶
- explorer
-
diff 方法优化
- Vue 2.x 是全量对比
- Vue 3.0 新增静态标记 (PatchFlag):
- Vue3.0中,在模版编译时,编译器会在 动态标签 末尾加上 /* Text */ PatchFlag。
- 每一个 Block 中的节点,就算很深,也是直接跟 Block 一层绑定的,可以直接跳转到动态节点而不需要逐个逐层遍历。
export const enum PatchFlags { TEXT = 1, // 1 动态文本节点 CLASS = 1 << 1, // 2 动态 class STYLE = 1 << 2, // 4 动态 style PROPS = 1 << 3, // 8 动态属性 FULL_PROPS = 1 << 4, // 16 具有动态 key 属性,当 key 变化,需要进行完整比较 HYDRATE_EVENTS = 1 << 5, // 32 带有事件监听器的节点 STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 fragment KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 fragment UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 fragment NEED_PATCH = 1 << 9, // 512 一个节点只进行非 props 比较 DYNAMIC_SLOTS = 1 << 10, // 1024 动态 // SPECIAL FLAGS HOISTED = -1, BAIL = -2 }
<div>Hello World!</div> <div>{{msg}}</div>
- hoistStatic 静态节点提升export function render(_ctx, _cache, $props, $setup, $data, $options) { return ( _openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, "Hello World!"), _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ], 64 /* STABLE_FRAGMENT */ ) ) }
- Vue 2 的节点不管是否参与更新,每次更新都会重新 _createVNode
- Vue 3 对于不参与更新的节点,只创建一次,缓存起来,之后的每次渲染复用缓存
- cacheHandler 事件监听缓存const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "Hello World!", -1 /* HOISTED */) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ], 64 /* STABLE_FRAGMENT */)) }
- 默认情况下,onClick 会被视为动态绑定,所以每次都会 watch 它的变化;
- 但因为是同一个函数,所以没有变化,直接缓存起来复用;
- 这个节点可以被看作一个静态节点。
<div>Hello World!</div> <div>{{msg}}</div> <button @click="handleClick">事件监听缓存</button>
// 不开启缓存 export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, "Hello World!"), _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), _createVNode("button", { onClick: _ctx.handleClick }, "事件监听缓存", 8 /* PROPS */, ["onClick"]) ], 64 /* STABLE_FRAGMENT */)) }
- SSR 渲染// 开启缓存 export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, "Hello World!"), _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), _createVNode("button", { onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handleClick(...args))) }, "事件监听缓存") ], 64 /* STABLE_FRAGMENT */)) }
- 当有大量 静态内容 时,内容会被当作纯字符串推进一个 buffer 里。
- 即使存在 动态绑,会通过模版差值嵌入其中,这个性能肯定比 React 转成 vDOM 再转化为 HTML 快很多。
-
StaticNode 静态节点
- 已知在 SSR 中静态的节点会被转化为纯字符串。
- 如果在 客户端,当静态节点嵌套足够多的时候,vue 3 编译器会用 _createStaticNode 方法生成字符串类型的 staticNode,直接innerHTML;不需要创建 vDOM 对象,然后根据对象渲染。